﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
using System.Xml;
using Castle.Core.Configuration;
using Castle.Core.Resource;
using Castle.MicroKernel;
using Castle.MicroKernel.SubSystems.Resource;

namespace Mbs.CastleContainer
{

    class XmlInterpreter
    {
        protected static readonly String FacilitiesNodeName = "facilities";
        protected static readonly String FacilityNodeName = "facility";
        protected static readonly String ComponentsNodeName = "components";
        protected static readonly String ComponentNodeName = "component";
        protected static readonly String IncludeNodeName = "include";
        protected static readonly String PropertiesNodeName = "properties";

        private IResource source;
        private Stack<IResource> resourceStack = new Stack<IResource>();


        public XmlInterpreter(IResource source)
        {
            if (source == null) throw new ArgumentNullException("source", "IResource is null");

            this.source = source;

            PushResource(source);
        }

        public XmlInterpreter(String filename)
            : this(new FileResource(filename))
        {
        }

        public XmlInterpreter()
        {
        }

        public IKernel Kernel { get; set; }

        public void Process(XmlNode node, IConfigurationStore store)
        {
            XmlProcessor processor = new XmlProcessor();

            try
            {
                XmlNode element = processor.Process(node);

                Deserialize(element, store);
            }
            catch (XmlProcessorException e)
            {
                throw new ConfigurationErrorsException("Unable to process xml resource ", e);
            }
        }

        public void ProcessResource(IResource source, IConfigurationStore store)
        {
            XmlProcessor processor = (Kernel == null) ?
                new XmlProcessor() :
                new XmlProcessor(
                    Kernel.GetSubSystem(SubSystemConstants.ResourceKey) as IResourceSubSystem);

            try
            {
                XmlNode element = processor.Process(source);

                Deserialize(element, store);
            }
            catch (XmlProcessorException e)
            {
                throw new ConfigurationErrorsException("Unable to process xml resource ", e);
            }
        }

        #region Deserialization methods

        protected void Deserialize(XmlNode section, IConfigurationStore store)
        {
            foreach (XmlNode node in section)
            {
                if (IsTextNode(node))
                {
                    throw new ConfigurationErrorsException(String.Format("{0} cannot contain text nodes", node.Name));
                }
                else if (node.NodeType == XmlNodeType.Element)
                {
                    DeserializeElement(node, store);
                }
            }
        }

        private void DeserializeElement(XmlNode node, IConfigurationStore store)
        {
            if (FacilitiesNodeName.Equals(node.Name))
            {
                DeserializeFacilities(node.ChildNodes, store);
            }
            else if (ComponentsNodeName.Equals(node.Name))
            {
                DeserializeComponents(node.ChildNodes, store);
            }
            else
            {
                throw new ConfigurationErrorsException(String.Format("DeserializeElement cannot process element {0}", node.Name));
            }
        }

        private void DeserializeFacilities(XmlNodeList nodes, IConfigurationStore store)
        {
            foreach (XmlNode node in nodes)
            {
                if (node.NodeType == XmlNodeType.Element)
                {
                    AssertNodeName(node, FacilityNodeName);

                    DeserializeFacility(node, store);
                }
            }
        }

        private void DeserializeFacility(XmlNode node, IConfigurationStore store)
        {

            String type = GetRequiredAttributeValue(node, "type");
            IConfiguration config = GetDeserializedNode(node);

            string id = config.Attributes["id"];
            if (string.IsNullOrEmpty(config.Attributes["id"]))
            {
                id = type;
                config.Attributes["id"] = id;
                (node as XmlElement).SetAttribute("id", id);
            }

            AddFacilityConfig(id, config, store);
        }

        private void DeserializeComponents(XmlNodeList nodes, IConfigurationStore store)
        {
            foreach (XmlNode node in nodes)
            {
                if (node.NodeType == XmlNodeType.Element)
                {
                    AssertNodeName(node, ComponentNodeName);

                    DeserializeComponent(node, store);
                }
            }
        }

        private void DeserializeComponent(XmlNode node, IConfigurationStore store)
        {
            String type = GetRequiredAttributeValue(node, "type");
            IConfiguration config = GetDeserializedNode(node);

            string id = config.Attributes["id"];
            if (string.IsNullOrEmpty(config.Attributes["id"]))
            {
                id = type;
                config.Attributes["id"] = id;
                (node as XmlElement).SetAttribute("id",id);
            }

            AddComponentConfig(id, config, store);
        }

        private IConfiguration GetDeserializedNode(XmlNode node)
        {
            MutableConfiguration config = null;
            ConfigurationCollection configChilds = new ConfigurationCollection();

            StringBuilder configValue = new StringBuilder();

            if (node.HasChildNodes)
            {
                foreach (XmlNode child in node.ChildNodes)
                {
                    if (IsTextNode(child))
                    {
                        configValue.Append(child.Value);
                    }
                    else if (child.NodeType == XmlNodeType.Element)
                    {
                        configChilds.Add(GetDeserializedNode(child));
                    }
                }
            }

            config = new MutableConfiguration(node.Name, GetConfigValue(configValue.ToString()));

            foreach (XmlAttribute attribute in node.Attributes)
            {
                config.Attributes.Add(attribute.Name, attribute.Value);
            }

            config.Children.AddRange(configChilds);

            return config;
        }

        #endregion

        /// <summary>
        /// If a config value is an empty string we return null, this is to keep
        /// backward compability with old code
        /// </summary>
        private string GetConfigValue(string value)
        {
            value = value.Trim();

            return value == String.Empty ? null : value;
        }

        private String GetRequiredAttributeValue(XmlNode node, String attName)
        {
            String value = GetAttributeValue(node, attName);

            if (value.Length == 0)
            {
                String message = String.Format("{0} elements expects required non blank attribute {1}",
                                               node.Name, attName);

                throw new ConfigurationErrorsException(message);
            }

            return value;
        }

        private String GetAttributeValue(XmlNode node, String attName)
        {
            XmlAttribute att = node.Attributes[attName];

            return (att == null) ? String.Empty : att.Value.Trim();
        }

        private bool IsTextNode(XmlNode node)
        {
            return node.NodeType == XmlNodeType.Text || node.NodeType == XmlNodeType.CDATA;
        }

        private void AssertRequiredAttribute(IConfiguration config, string attrName, string parentName)
        {
            String content = config.Attributes[attrName];

            if (content == null || content.Trim() == String.Empty)
            {
                throw new ConfigurationErrorsException(String.Format("{0} expects {1} attribute", parentName, attrName));
            }
        }

        private void AssertNodeName(XmlNode node, string expectedName)
        {
            if (!expectedName.Equals(node.Name))
            {
                String message = String.Format("Unexpected element under '{0}': Expected '{1}' but found '{2}'",
                                               expectedName, expectedName, node.Name);

                throw new ConfigurationErrorsException(message);
            }
        }

        protected void PushResource(IResource resource)
        {
            resourceStack.Push(resource);
        }

        protected void PopResource()
        {
            resourceStack.Pop();
        }

        protected IResource CurrentResource
        {
            get
            {
                if (resourceStack.Count == 0) return null;

                return resourceStack.Peek() as IResource;
            }
        }


        public IResource Source
        {
            get { return this.source; }
        }


        protected void AddFacilityConfig(IConfiguration facility, IConfigurationStore store)
        {
            AddFacilityConfig(facility.Attributes["id"], facility, store);
        }

        protected void AddComponentConfig(IConfiguration component, IConfigurationStore store)
        {
            AddComponentConfig(component.Attributes["id"], component, store);
        }

        protected void AddFacilityConfig(String id, IConfiguration facility, IConfigurationStore store)
        {
            if (string.IsNullOrEmpty(id))
            {
                facility.Attributes["id"] = facility.Attributes["type"];
                id = facility.Attributes["type"];
            }

            // TODO: Use import collection on type attribute (if it exists)

            store.AddFacilityConfiguration(id, facility);
        }

        protected void AddComponentConfig(String id, IConfiguration component, IConfigurationStore store)
        {
            if (string.IsNullOrEmpty(id))
            {
                component.Attributes["id"] = component.Attributes["type"];
                id = component.Attributes["type"];
            }

            // TODO: Use import collection on type and service attribute (if they exist)

            store.AddComponentConfiguration(id, component);
        }

        private void AssertValidId(string id)
        {
            if (id == null || id.Length == 0)
            {
                throw new ConfigurationErrorsException("Component or Facility was declared without a proper 'id' attribute");
            }
        }


        protected void ProcessInclude(String uri, IConfigurationStore store)
        {
            IResource resource = store.GetResource(uri, CurrentResource);

            if (resource == null)
            {
                // TODO: Proper Exception
            }

            PushResource(resource);

            ProcessResource(resource, store);

            PopResource();
        }
    }

}
